package com.iakuil.app.common.restful;

import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import com.iakuil.app.common.core.annotation.ErrorCode;
import com.iakuil.app.common.core.domain.Result;
import com.iakuil.app.common.core.enums.Results;
import com.iakuil.app.common.core.exception.BusinessException;
import com.iakuil.app.common.core.exception.SystemException;
import com.iakuil.app.common.core.exception.ThirdPartyApiException;
import com.iakuil.app.common.core.util.MapUtils;
import com.iakuil.app.common.core.util.RequestIdUtils;
import com.iakuil.app.common.core.util.StringUtils;
import io.swagger.v3.oas.annotations.Hidden;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.resource.NoResourceFoundException;

import java.lang.reflect.Field;
import java.sql.SQLException;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 全局异常处理
 *
 * @author Kai
 */
@Slf4j
@Hidden
@ControllerAdvice
public class GlobalExceptionHandler {
    private static final String DEFAULT_ERROR_MESSAGE = "Occurring an unknown exception! Please check with admin if you want to get the details!";

    @ExceptionHandler(BusinessException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result<Void> processException(BusinessException e) {
        // 业务异常返回友好的错误码和提示文字，而不是只有程序员才能看懂的异常信息
        String message = e.getMessage();
        return Result.error(e.getCode(), message == null ? DEFAULT_ERROR_MESSAGE : message);
    }

    @ExceptionHandler(ThirdPartyApiException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result<Void> processException(ThirdPartyApiException e) {
        String msg = e.getMessage();
        Map<String, Object> extra = e.getExtra();
        if (extra != null) {
            msg += MapUtils.join(extra, ",", ":");
        }
        // 第三方接口异常，仅打印接口返回值
        log.error("[REMOTE ERROR] {}", msg);
        return Result.error(e.getCode(), DEFAULT_ERROR_MESSAGE);
    }

    @ExceptionHandler(SystemException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result<Void> processException(SystemException e) {
        // 系统业务异常，打印全量堆栈信息
        log.error("[SYSTEM ERROR] ", e);
        return Result.error(e.getCode(), DEFAULT_ERROR_MESSAGE);
    }

    @ExceptionHandler(MissingServletRequestParameterException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(MissingServletRequestParameterException e) {
        log.error("[PARAM ERROR] {}", e.getMessage());
        // e.g. Required String parameter 'xxx' is not present
        return Result.error(Results.BAD_REQUEST, "缺少必要的请求参数：" + e.getParameterName());
    }

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Object> processException(MethodArgumentTypeMismatchException e) {
        log.error("[PARAM ERROR] {}", e.getMessage());
        return Result.error(Results.BAD_REQUEST, "请求参数[" + e.getName() + "]期望的类型是：" + e.getRequiredType() + "。");
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(MethodArgumentNotValidException e) throws NoSuchFieldException {
        log.error("[PARAM ERROR] {}", e.getMessage());
        Integer code = null;
        String msg = null;
        FieldError fieldError = e.getBindingResult().getFieldError();
        if (fieldError != null) {
            String fieldName = fieldError.getField();
            Class<?> parameterType = e.getParameter().getParameterType();
            Field field = parameterType.getDeclaredField(fieldName);
            // 如果有自定义的错误码注解则优先使用
            ErrorCode annotation = field.getAnnotation(ErrorCode.class);
            if (annotation != null && annotation.value() != -1) {
                code = annotation.value();
            }
            if (annotation != null && StringUtils.isNoneBlank(annotation.message())) {
                msg = annotation.message();
            }
        }

        return Result.error(code == null ? Results.BAD_REQUEST.getCode() : code, StringUtils.defaultIfBlank(msg, e.getBindingResult()
            .getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining("；"))));
    }

    @ExceptionHandler(BindException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(BindException e) {
        log.error("[PARAM ERROR]", e);
        // e.g. 入参加了@Valid注解，但是忘了加@RequestBody注解，一般出现在开发阶段
        String message = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
        return Result.error(Results.BAD_REQUEST, message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(ConstraintViolationException e) {
        log.error("[PARAM ERROR]", e);
        // e.g. 入参加了@Valided注解，但是忘了加@RequestBody注解，一般出现在开发阶段
        String message = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
        return Result.error(Results.BAD_REQUEST, message);
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(HttpMessageNotReadableException e) {
        log.error("[PARAM ERROR] {}", e.getMessage());
        // e.g. JSON Body格式错误
        return Result.error(Results.BAD_REQUEST, Results.BAD_REQUEST.getMessage());
    }

    @ExceptionHandler(HandlerMethodValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseBody
    public Result<Void> processException(HandlerMethodValidationException e) {
        log.error("[PARAM ERROR] {}", e.getMessage());
        Object[] msgArgs = e.getDetailMessageArguments();
        return Result.error(Results.BAD_REQUEST.getCode(), msgArgs == null ? Results.BAD_REQUEST.getMessage() : msgArgs[0].toString());
    }

    @ExceptionHandler(NoResourceFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ResponseBody
    public Result<Void> processException(NoResourceFoundException e) {
        log.error("[REQUEST ERROR] {}", e.getMessage());
        return Result.error(Results.NOT_FOUND, Results.NOT_FOUND.getMessage());
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
    @ResponseBody
    public Result<Object> processException(HttpRequestMethodNotSupportedException e) {
        log.error("[REQUEST ERROR] {}", e.getMessage());
        return Result.error(Results.METHOD_NOT_SUPPORTED, e.getMessage());
    }

    @ExceptionHandler(HttpMediaTypeNotSupportedException.class)
    @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
    @ResponseBody
    public Result<Object> processException(HttpMediaTypeNotSupportedException e) {
        log.error("[REQUEST ERROR] {}", e.getMessage());
        return Result.error(Results.MEDIA_TYPE_NOT_SUPPORTED, e.getMessage());
    }

    @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
    @ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
    @ResponseBody
    public Result<Object> processException(HttpMediaTypeNotAcceptableException e) {
        log.error("[REQUEST ERROR] {}", e.getMessage());
        // 现在一般都是RestController，所以很少遇到，解决方案只需要在请求头增加：Accept: application/json, text/plain
        return Result.error(Results.NOT_ACCEPTABLE, e.getMessage());
    }

    @ExceptionHandler(SQLException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result<Void> processException(SQLException e) {
        log.error("[DB ERROR]", e);
        return Result.error(Results.INTERNAL_SERVER_ERROR, DEFAULT_ERROR_MESSAGE);
    }

    @ExceptionHandler(NotLoginException.class)
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ResponseBody
    public Result<Void> processException(NotLoginException e) {
        // 权限体系比较简单，统一在此处理
        log.error("[AUTH ERROR]{}", e.getMessage());
        return Result.error(Results.UNAUTHORIZED, "用户未登录");
    }

    @ExceptionHandler(NotPermissionException.class)
    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ResponseBody
    public Result<Void> processException(NotPermissionException e) {
        log.error("[AUTH ERROR]{}", e.getMessage());
        return Result.error(Results.FORBIDDEN, "无权限");
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ResponseBody
    public Result<Void> processException(Exception e) {
        log.error("[UNKNOWN ERROR]", e);
        // 未知异常兜底
        Result<Void> result = Result.error(Results.INTERNAL_SERVER_ERROR, DEFAULT_ERROR_MESSAGE);
        result.put("rid", RequestIdUtils.getRequestId());
        return result;
    }
}
